﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Web.Caching;
using System.Web.UI;
using Univar.Helpers;

namespace Univar
{
	/// <summary>
	/// A class used to read/write local variables from any local sources like a cookie, query string,
	/// session or cache.
	/// </summary>
	/// <typeparam key="T">The generic type T for the variable.</typeparam>
	public class DynamicStore<T> : StoreBase<T, DynamicStore<T>>
	{
		public string DefaultUsername { get; set; }

		public Sources LastReadStorage = Sources.None;
		public Sources[] _sourceStorages;
		public int? _targetStorages;

		public bool IsEncrypted { get; set; }
		public bool IsCompressed { get; set; }

		// Query string properties


		// Cookie properties 

		public TimeSpan CookieLifeTime { get; set; }
		public string CookiePath { get; set; }
		public string CookieDomain { get; set; }
		public bool IsCookieHttpOnly { get; set; }
		public bool IsSecureCookie { get; set; }

		// Cache properties

		public TimeSpan CacheLifeTime { get; set; }
		public CacheItemRemovedCallback CacheItemRemovedCallback { get; set; }
		public CacheItemPriority CacheItemPriority { get; set; }
		public CacheDependency CacheDependencies { get; set; }

		// InCacheFile properties

		public string InFileCacheFolderPath { get; set; } // This can be used to override the default folder path.
		public TimeSpan? InFileCacheLifeTime { get; set; }

		public TimeSpan CookieAndCacheLifeTime
		{
			set { CacheLifeTime = CookieLifeTime = value; }
		}

		/// <summary>
		/// This enables user identification via the membership provider for session file storage.
		/// When true the id of the user is always retrieved from the client cookie.
		/// </summary>
		public bool UseProfileNameIfAvailable { get; set; }

		public DynamicStore(string key, params Sources[] sourceStorages)
			: this(default(T), key, null, null, null, null, sourceStorages) { }

		public DynamicStore(string key, int? targetStorages, params Sources[] sourceStorages)
			: this(default(T), key, null, null, null, targetStorages, sourceStorages) { }

		public DynamicStore(T defaultValue, string key, params Sources[] sourceStorages)
			: this(defaultValue, key, null, null, null, null, sourceStorages) { }

		public DynamicStore(T defaultValue, string key, int? targetStorages, params Sources[] sourceStorages)
			: this(defaultValue, key, null, null, null, targetStorages, sourceStorages) { }

		/// <summary>
		/// Initializes a new instance of the <see cref="DynamicStore&lt;T&gt;"/> class.
		/// </summary>
		/// <param name="defaultValue">The default value used when the specified key does not exist in the 
		/// specified sources.</param>
		/// <param name="key">The key for the stored value.</param>
		/// <param name="cookieLifeTime">
		/// The cookie life time. Its value is set to 100 days when a null value is specified.</param>
		/// <param name="cacheLifeTime">
		/// The cache life time. Its value is set to 20 minutes when a null value is specified.</param>
		/// <param name="targetStorages">Specifies the storage types where the value is allowed to be saved. 
		/// Those are hardcoded in the Targets class. Several types can be specified
		/// by adding them up using the '+' operator(e.g Targets.Cookie + Targets.Session)</param>
		/// <param name="sourcesStorages">A list of storage types from which the value will be retrieved
		/// Each type is hardcoded within the Sources enum and they must be seperated by a comma.
		/// The order in which they are listed determines the order in which they are accessed until a value is found.
		/// </param>
		public DynamicStore(T defaultValue, string key, TimeSpan? cookieLifeTime, CacheDependency cacheDependency,
			TimeSpan? cacheLifeTime, int? targetStoragesFlags, params Sources[] sourcesStorages)
		{
			UseProfileNameIfAvailable = true;
			_isConnected = true;
			_key = key;
			// Use Sources.Session as default when no source is specified.
			SourceStorages = sourcesStorages.Length > 0 ? sourcesStorages : new Sources[] { Sources.Session };
			TargetStorages = targetStoragesFlags;
			DefaultValue = defaultValue;

			CookieLifeTime = cookieLifeTime ?? Storage.Cookie.DefaultLifeTime;
			CacheLifeTime = cacheLifeTime ?? Storage.Cache.DefaultLifeTime;
			CacheItemPriority = CacheItemPriority.Default;
			CacheDependencies = cacheDependency;
			InFileCacheLifeTime = InFileCacheLifeTime ?? Storage.InFileCache.DefaultLifeTime;
		}

		/// <summary>
		/// The sum of the target types in which the values is saved.
		/// </summary>
		public int? TargetStorages
		{
			get
			{
				return _targetStorages;
			}
			set
			{
				if (value.HasValue)
					_targetStorages = value;
				else
					// Set the targets as the sources when none is specified.
					_targetStorages = GetSourcesSum(SourceStorages);
			}
		}

		/// <summary>
		/// An array of sources to be used then retrieving the value.
		/// </summary>
		public Sources[] SourceStorages
		{
			get
			{
				return _sourceStorages;
			}
			set
			{
				_sourceStorages = value;
				// Set the target types as the source types if it is null.
				if (!TargetStorages.HasValue)
					_targetStorages = GetSourcesSum(value);
			}
		}

		private int? GetSourcesSum(Sources[] sources)
		{
			int? sum = null;
			// Add all sources to the targetStorages property.
			foreach (Sources source in sources)
			{
				// When the source type is set as None avoid assigning the target with the same value
				// since this would make our Value property readonly.
				if (source != Sources.None)
					sum = (sum ?? 0) + (int)source;
			}
			return sum;

		}

		public Type ValueType
		{
			get { return typeof(T); }
		}

		/// <summary>
		/// Identifies the first available source containing the specified key.
		/// This is the source that is used to fetch the value.
		/// </summary>
		public Sources CurrentSource
		{
			get
			{
				Sources source = Sources.None;
				GetValue<object>(null, out source, _sourceStorages);
				return source;
			}
		}

		/// <summary>
		/// Gets the value from the first source containing the specified key.
		/// An array of sources can be specified using the sourceStorages parameter.
		/// The default source used is the session when none is specified.
		/// </summary>
		/// <param name="key">The key under which the value is stored.</param>
		/// <returns>An object of type T representing the value.</returns>
		protected override T GetValue(string key)
		{
			Sources dummySource;
			T value = GetValue<T>(key, out dummySource, _sourceStorages);
			if (dummySource != Sources.None)
				return value;
			else
				return DefaultValue;
		}

		/// <summary>
		/// Gets the value in its undeserialized state.
		/// An array of storage sources can be specified using the sourceStorages parameter.
		/// The default storage source used is the session when none is specified.
		/// </summary>
		/// <param name="key">A nullable value specifying the child key under which the value is stored.</param>
		/// <param name="storageSourceUsed">Returns the storage type from which the value was retrieved.
		/// Sources.None is returned when the key is inexistant.</param>
		/// <returns>An object representing the value.</returns>
		protected T1 GetValue<T1>(string key, out Sources storageSourceUsed, params Sources[] sourceStorages)
		{
			T1 value = default(T1);
			storageSourceUsed = Sources.None;
			// Loop through each source type until the specified key is found.
			foreach (Sources storageSource in sourceStorages)
			{
				switch (storageSource)
				{
					case Sources.Session:
						value = GetValue<T1>(key, Scopes.Default, Sources.Session);
						break;

					case Sources.LocationBoundSession:
						value = GetValue<T1>(key, Scopes.Location, Sources.Session);
						break;

					case Sources.Cookie:
						value = GetValue<T1>(key, Scopes.Default, Sources.Cookie);
						break;

					case Sources.LocationBoundCookie:
						value = GetValue<T1>(key, Scopes.Location, Sources.Cookie);
						break;

					case Sources.UserBoundCookie:
						value = GetValue<T1>(key, Scopes.User, Sources.Cookie);
						break;

					case Sources.Cache:
						value = GetValue<T1>(key, Scopes.Default, Sources.Cache);
						break;

					case Sources.LocationBoundCache:
						value = GetValue<T1>(key, Scopes.Location, Sources.Cache);
						break;

					case Sources.UserBoundCache:
						value = GetValue<T1>(key, Scopes.User, Sources.Cache);
						break;

					case Sources.BrowserBoundCache:
						value = GetValue<T1>(key, Scopes.Browser, Sources.Cache);
						break;

					case Sources.InFileCache:
						value = GetValue<T1>(key, Scopes.Location, Sources.InFileCache);
						break;

					case Sources.LocationBoundInFileCache:
						value = GetValue<T1>(key, Scopes.Location, Sources.InFileCache);
						break;

					case Sources.UserBoundInFileCache:
						value = GetValue<T1>(key, Scopes.User, Sources.InFileCache);
						break;

					case Sources.BrowserBoundInFileCache:
						value = GetValue<T1>(key, Scopes.Browser, Sources.InFileCache);
						break;

					//case Sources.Memcached:
					//    value = MemCachedClient.Get(key);
					//    break;		

					case Sources.QueryString:
						value = GetValue<T1>(key, Scopes.Default, Sources.QueryString);
						break;
				}

				if (value != null)
				{
					storageSourceUsed = storageSource;
					break;
				}
			}

			LastReadStorage = storageSourceUsed;

			return value;
		}

		private T1 GetValue<T1>(string key, Scopes scope, Sources source)
		{
			string parentKey = StorageUser.GetUserKeyByScope(null, scope, HttpContext, TimeSpan.MaxValue);
			T1 value = default(T1);


			if (parentKey != null)
			{
				switch (source)
				{
					case Sources.Session:
						value = Storage.Session.Get<T1>(parentKey + key, HttpContext);
						break;
					case Sources.Cookie:
						value = Storage.Cookie.Get<T1>(parentKey + key, IsCompressed, IsEncrypted, ThrowExceptionOnDeserializationOrDecryptionError);
						break;
					case Sources.Cache:
						value = Storage.Cache.Get<T1>(parentKey + key, HttpContext);
						break;
					case Sources.InFileCache:
						value = Storage.InFileCache.Get<T1>(InFileCacheFolderPath, parentKey ?? "Shared", key, IsEncrypted);
						break;
					case Sources.QueryString:
						value = Storage.QueryString.Get<T1>(parentKey + key, IsCompressed, IsEncrypted, ThrowExceptionOnDeserializationOrDecryptionError);
						break;
				}
			}
			return value;
		}

		protected override void SetValue(string key, T value)
		{
			SetValue<T>(key, value);
		}


		/// <summary>
		/// Set the specified value to all target storages in the targetStorages flags.
		/// </summary>
		/// <typeparam name="T1">The type of the value.</typeparam>
		/// <param name="key">A nullable value specifying the key under which the value is stored.</param>
		/// <param name="value">The value to be stored.</param>
		protected void SetValue<T1>(string key, T1 value)
		{
			int flags = TargetStorages ?? 0;

			if ((flags & Targets.Session) != 0)
				SetValue(key, value, Scopes.Default, Targets.Session);

			if ((flags & Targets.LocationBoundSession) != 0)
				SetValue(key, value, Scopes.Location, Targets.Session);

			if ((flags & Targets.Cookie) != 0)
				SetValue(key, value, Scopes.Default, Targets.Cookie);

			if ((flags & Targets.LocationBoundCookie) != 0)
				SetValue(key, value, Scopes.Location, Targets.Cookie);

			if ((flags & Targets.UserBoundCookie) != 0)
				SetValue(key, value, Scopes.User, Targets.Cookie);

			if ((flags & Targets.Cache) != 0)
				SetValue(key, value, Scopes.Default, Targets.Cache);

			if ((flags & Targets.LocationBoundCache) != 0)
				SetValue(key, value, Scopes.Location, Targets.Cache);

			if ((flags & Targets.UserBoundCache) != 0)
				SetValue(key, value, Scopes.User, Targets.Cache);

			if ((flags & Targets.BrowserBoundCache) != 0)
				SetValue(key, value, Scopes.Browser, Targets.Cache);

			if ((flags & Targets.InFileCache) != 0)
				SetValue(key, value, Scopes.Default, Targets.InFileCache);

			if ((flags & Targets.LocationBoundInFileCache) != 0)
				SetValue(key, value, Scopes.Location, Targets.InFileCache);

			if ((flags & Targets.UserBoundInFileCache) != 0)
				SetValue(key, value, Scopes.User, Targets.InFileCache);

			if ((flags & Targets.BrowserBoundInFileCache) != 0)
				SetValue(key, value, Scopes.Browser, Targets.InFileCache);

			//if ((flags & Targets.Memcached) != 0)
			//    MemCachedClient.Store(Enyim.Caching.Memcached.StoreMode.Set, key, value);
			//if ((flags & Targets.Anchor) != 0)
			//    SetAnchor( BuildString(true, key, value).ToString());


			// The query string is always set last since it uses the Redirect method to do so.
			// This can potentially end the thread thus skipping all operations on any remaining targeted storages.
			if ((flags & Targets.QueryString) != 0)
				SetValue(key, value, Scopes.Default, Targets.QueryString);
		}

		private void SetValue<T1>(string key, T1 value, Scopes scope, int target)
		{
			string parentKey = StorageUser.GetUserKeyByScope(null, scope, HttpContext, TimeSpan.MaxValue);

			if (parentKey != null)
			{
				switch (target)
				{
					case Targets.Session:
						Storage.Session.Set(parentKey + key, value, HttpContext);
						break;
					case Targets.Cookie:
						Storage.Cookie.Set<T1>(parentKey + key, value, CookieLifeTime, IsCompressed, IsEncrypted,
							CookiePath, CookieDomain, IsCookieHttpOnly, IsSecureCookie, ThrowExceptionOnDeserializationOrDecryptionError);
						break;
					case Targets.Cache:
						Storage.Cache.Set(parentKey + key, value, HttpContext, CacheLifeTime, CacheDependencies,
							CacheItemPriority, CacheItemRemovedCallback);
						break;
					case Targets.InFileCache:
						Storage.InFileCache.Set<T1>(InFileCacheFolderPath, parentKey ?? "Shared"
							, key, value, InFileCacheLifeTime, IsEncrypted, ThrowExceptionOnDeserializationOrDecryptionError);
						break;
					case Targets.QueryString:
						Storage.QueryString.Set<T1>(parentKey + key, value, IsCompressed, IsEncrypted);
						break;
				}
			}
		}

		protected override object PlainValue
		{
			get
			{
				Sources dummySource;
				return GetValue<T>(Key, out dummySource, _sourceStorages);
			}
			set
			{
				SetValue<object>(Key, value);
			}
		}

		/// <summary>
		/// Copies the value from the first available source in the sources specified to all the target types.
		/// </summary>
		public void CopySourceToAllTargets()
		{
			this.Value = GetValue(_key);
		}

		public void CopySourceTo(int targetStorages)
		{
			DynamicStore<T> dummyVar = this.ShallowClone();
			dummyVar.TargetStorages = targetStorages;
			dummyVar.CopySourceToAllTargets();
		}

		public void MoveAllSourcesTo(int targetStorages)
		{
			T value = this.Value;
			this.Value = DefaultValue;
			DynamicStore<T> dummyVar = this.ShallowClone();
			dummyVar.TargetStorages = targetStorages;
			dummyVar.Value = value;
		}

		public override void Clear()
		{
			new DynamicStore<object>(Key, TargetStorages, SourceStorages).Value = null;
		}

		public void ClearSourcesOnly()
		{
			foreach (Sources source in SourceStorages)
				new DynamicStore<object>(_key, source).Value = null;
		}

		/// <summary>
		/// Removes the key and its value in the specified target types.
		/// </summary>
		public void ClearTargetsOnly()
		{
			new DynamicStore<object>(_key, TargetStorages, Sources.None).Value = null;
		}

		public bool TargetStoragesInclude(params int[] targetStorages)
		{
			return (TargetStorages & targetStorages.Sum()) != 0;
		}

		public bool SourceStoragesIncludes(Sources storageSource)
		{
			return SourceStorages.Contains(storageSource);
		}
	}

}